iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
0
自我挑戰組

iOS 新手開發的大小事系列 第 13

Day 13: 自動引用計數 (Automatic Reference Counting) -1

  • 分享至 

  • xImage
  •  

前言

今天要來介紹的是用 Swift 開發時所使用管理記憶體的方法,這個觀念其實滿重要的,為的是避免在開發時,寫出來的代碼,造成未知的記憶體洩漏 (Memory Leak),但由於此章節內容較多,所以預計分成三天來撰寫。

Swift 為現代高階語言,對於記憶體有一套管理方法,即 Automatic Reference Counting -- ARC 來追蹤跟管理 app 的內存使用狀況,在大多數情況下,開發者在開發時就不需多費心力在處理記憶體的部分。當不在需要這些實例時,ARC 會自動釋放類實例使用的內存。

但是,在少數情況下,ARC 需要有關代碼部分之間關係的更多信息,以便為開發者管理內存,開發者也是有責任需要了解記憶體的管理,否則容易造成記憶體洩漏 (memory leak)。

引用計數僅適用於類的實例。結構和列舉是值類型,而不是引用類型,並且不使用引用存儲和傳遞。


ARC 的運作方式

每次創建類的新實例時,ARC 都會分配一塊內存來存儲有關該實例的資料。此內存保存有關實例類型的資料,以及與該實例相關聯的任何存儲屬性的值。

另外,當不再需要實例時,ARC 釋放該實例使用的內存,以便可以將內存用於其他目的。這可確保類實例在不再需要時不會佔用內存空間。

但是,如果 ARC 要釋放仍在使用的實例,則將無法再訪問該實例的屬性,或者調用該實例的方法。實際上,如果我們嘗試訪問該實例,則 app 很可能會崩潰。

為了確保實例在仍然需要時不會消失,ARC 會跟踪當前引用每個類實例的屬性、常數或變數的數量。只要至少有一個對該實例的引用仍然存在,ARC 就不會解除分配實例。

為了實現這一點,無論何時將類實例分配給屬性、常數或變數,該屬性、常數或變數都會對實例進行強引用。該引用被稱為「強」引用,因為它保持牢牢地抓住該實例,並且只要該強引用仍然存在就不允許它被釋放。


ARC 的執行

用以下的範例來解釋 ARC 的運作,先簡單的定義名為 Person 的類,內部儲存一個 name 的常數:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}
var reference1: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

以上例子將 reference1 定義為 Person 的可選型別 (optional) 形式,則 reference1 初始值則為 nil,最後一行為創建一個新實例,並給入參數值,則會印出類被初始化。

接著我們再將定義一個也是為 Person 可選型別形式的變數,並將 reference1 指定給這個變數,接著將 reference1 指定為 nil

var reference2: Person?
reference2 = reference1

reference1 = nil

此時,Person 的實例尚未被釋放掉,最後再將 reference2 也指定為 nil 時,實例的記憶體才會被釋放掉:

reference2 = nil
// Prints "John Appleseed is being deinitialized"

類實例之間的強引用循環 (Strong Reference Cycles Between Class Instances)

在上述的範例中,ARC 能追蹤我們創建的新 Person 實例的引用數目,並在不再需要時釋放該 Person 實例。

但是,可以編寫一個代碼,其中類的實例永遠不會為零的強引用程度。如果兩個類實例彼此擁有強引用,這樣每個實例都會將另一個實例保持存活狀態。這被稱為強引用循環 (Strong Reference Cycles)。

可以通過將類之間的某些關係定義為弱引用 (weak references) 或無主引用 (unowned references) 而不是強引用來解決強引用循環。

以下為強引用循環的例子,這個例子定義了兩個名為 PersonApartment 的類,它們模擬了一套公寓及其居民:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

每一個 Person 的實例都擁有一個型別為 String 的屬性 name 和一個初始值為 nil 屬性為 apartment 的可選型別。

同樣的,每一個 Apartment 的實例中有一個型別為 String 的屬性 unit 和一個初始值為 nil 的屬性 tenant 可選型別。租戶 (tenant) 屬性是可選型別,因為公寓可能並不是都有租戶。

這兩個類還定義了一個反初始化器,它印出該類的一個實例被取消初始化的事實。這使我們可以查看 PersonApartment 的實例是否按預期釋放。

接著將兩個變數指定為 Person 的可選型別和Apartment 的可選型別,因可選型別的值可設為 nil,並帶入初始值:

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

下圖解釋了創建和分配這兩個實例後強引用的狀況,變數 john 對新 Person 實例的強引用,及變數 unit4A 對新 Apartment 實例的強引用。

接著將 johnapartment 指定為 unit4A,和把 unit4Atenant 指定為 john

john!.apartment = unit4A
unit4A!.tenant = john

此時兩個實例連接在一起後強引用的情形:

不幸的是,連接這兩個實例會在它們之間產生強大的參考循環。 Person 實例現在具有對 Apartment 實例的強引用,並且 Apartment 實例具有對 Person 實例的強引用。因此,當我們中斷 johnunit4A 變量所持有的強引用時,引用計數不會降為零,並且 ARC 不會釋放實例:

john = nil
unit4A = nil

請注意,當我們將這兩個變數設置為 nil 時,都不會調用反初始化器。強引用循環可防止 PersonApartment 實例被解除分配記憶體,進而導致 app 內存洩漏。


上一篇
Day 12: [Swift] 錯誤處理 (Error Handling)
下一篇
Day 14: 自動引用計數 (Automatic Reference Counting) -2
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言